//
//  FileHandle.swift
//  AMSMB2
//
//  Created by Amir Abbas on 5/20/18.
//  Copyright © 2018 Mousavian. Distributed under MIT license.
//  All rights reserved.
//

import Foundation
import SMB2

typealias smb2fh = OpaquePointer

#if os(Linux) || os(Android) || os(OpenBSD)
let O_SYMLINK: Int32 = O_NOFOLLOW
#endif

final class SMB2FileHandle {
    private var context: SMB2Context
    private var handle: smb2fh?

    convenience init(forReadingAtPath path: String, on context: SMB2Context) throws {
        try self.init(path, flags: O_RDONLY, on: context)
    }

    convenience init(forWritingAtPath path: String, on context: SMB2Context) throws {
        try self.init(path, flags: O_WRONLY, on: context)
    }

    convenience init(forUpdatingAtPath path: String, on context: SMB2Context) throws {
        try self.init(path, flags: O_RDWR | O_APPEND, on: context)
    }

    convenience init(forOverwritingAtPath path: String, on context: SMB2Context) throws {
        try self.init(path, flags: O_WRONLY | O_CREAT | O_TRUNC, on: context)
    }

    convenience init(forOutputAtPath path: String, on context: SMB2Context) throws {
        try self.init(path, flags: O_WRONLY | O_CREAT, on: context)
    }
    
    convenience init(forCreatingIfNotExistsAtPath path: String, on context: SMB2Context) throws {
        try self.init(path, flags: O_RDWR | O_CREAT | O_EXCL, on: context)
    }

    static func using(
        path: String,
        opLock: OpLock = .none,
        impersonation: ImpersonationLevel = .impersonation,
        desiredAccess: Access = [
            .readData, .writeData, .appendData, .fileReadEA, .fileReadAttributes, .fileWriteEA,
            .fileWriteAttributes, .readControl, .synchronize
        ],
        fileAttributes: Attributes = [],
        shareAccess: ShareAccess = [.read, .write, .delete],
        createDisposition: CreateDisposition = .open,
        createOptions: CreateOptions = [], on context: SMB2Context
    ) throws -> SMB2FileHandle {
        let (_, result) = try context.async_await_pdu(dataHandler: SMB2FileHandle.init) {
            context, cbPtr -> UnsafeMutablePointer<smb2_pdu>? in
            path.replacingOccurrences(of: "/", with: "\\").withCString { path in
                var req = smb2_create_request()
                req.requested_oplock_level = opLock.rawValue
                req.impersonation_level = impersonation.rawValue
                req.desired_access = desiredAccess.rawValue
                req.file_attributes = fileAttributes.rawValue
                req.share_access = shareAccess.rawValue
                req.create_disposition = createDisposition.rawValue
                req.create_options = createOptions.rawValue
                req.name = path
                return smb2_cmd_create_async(context, &req, SMB2Context.generic_handler, cbPtr)
            }
        }

        return result
    }
    
    static func open(path: String, flags: Int32, on context: SMB2Context) throws -> SMB2FileHandle {
        let desiredAccess: Access
        let shareAccess: ShareAccess
        let createDisposition: CreateDisposition
        var createOptions: CreateOptions = []
        
        switch flags & O_ACCMODE {
        case O_RDWR:
            desiredAccess = [.genericRead, .genericWrite, .delete]
            shareAccess = [.read, .write]
        case O_WRONLY:
            desiredAccess = [.genericWrite, .delete]
            shareAccess = [.write]
        default:
            desiredAccess = [.genericRead]
            shareAccess = [.read]
        }
        
        if (flags & O_CREAT) != 0 {
            if (flags & O_EXCL) != 0 {
                createDisposition = .create
            } else if (flags & O_TRUNC) != 0 {
                createDisposition = .overwriteIfExists
            } else {
                createDisposition = .openIfExists
            }
        } else {
            if (flags & O_TRUNC) != 0 {
                createDisposition = .overwrite
            } else {
                createDisposition = .open
            }
        }
        
        if (flags & O_DIRECTORY) != 0 {
            createOptions.insert(.directoryFile)
        }
        if (flags & O_SYMLINK) != 0 {
            createOptions.insert(.openReparsePoint)
        }
        
        return try SMB2FileHandle.using(
            path: path,
            desiredAccess: desiredAccess,
            shareAccess: shareAccess,
            createDisposition: createDisposition,
            createOptions: createOptions,
            on: context
        )
    }

    init(fileDescriptor: smb2_file_id, on context: SMB2Context) throws {
        self.context = context
        var fileDescriptor = fileDescriptor
        self.handle = smb2_fh_from_file_id(context.unsafeContext, &fileDescriptor)
    }

    private init(_ path: String, flags: Int32, on context: SMB2Context) throws {
        let (_, handle) = try context.async_await(dataHandler: OpaquePointer.init) {
            context, cbPtr -> Int32 in
            smb2_open_async(context, path.canonical, flags, SMB2Context.generic_handler, cbPtr)
        }
        self.context = context
        self.handle = handle
    }

    deinit {
        do {
            let handle = try self.handle.unwrap()
            try context.async_await { context, cbPtr -> Int32 in
                smb2_close_async(context, handle, SMB2Context.generic_handler, cbPtr)
            }
        } catch {}
    }

    var fileId: UUID {
        .init(uuid: (try? smb2_get_file_id(handle.unwrap()).unwrap().pointee) ?? compound_file_id)
    }

    func close() {
        guard let handle = handle else { return }
        self.handle = nil
        _ = try? context.withThreadSafeContext { context in
            smb2_close(context, handle)
        }
    }

    func fstat() throws -> smb2_stat_64 {
        let handle = try handle.unwrap()
        var st = smb2_stat_64()
        try context.async_await { context, cbPtr -> Int32 in
            smb2_fstat_async(context, handle, &st, SMB2Context.generic_handler, cbPtr)
        }
        return st
    }
    
    func set(stat: smb2_stat_64, attributes: Attributes) throws {
        let handle = try handle.unwrap()
        try context.async_await_pdu(dataHandler: EmptyReply.init) {
            context, cbPtr -> UnsafeMutablePointer<smb2_pdu>? in
            var bfi = smb2_file_basic_info(
                creation_time: smb2_timeval(
                    tv_sec: .init(stat.smb2_btime),
                    tv_usec: .init(stat.smb2_btime_nsec / 1000)
                ),
                last_access_time: smb2_timeval(
                    tv_sec: .init(stat.smb2_atime),
                    tv_usec: .init(stat.smb2_atime_nsec / 1000)
                ),
                last_write_time: smb2_timeval(
                    tv_sec: .init(stat.smb2_mtime),
                    tv_usec: .init(stat.smb2_mtime_nsec / 1000)
                ),
                change_time: smb2_timeval(
                    tv_sec: .init(stat.smb2_ctime),
                    tv_usec: .init(stat.smb2_ctime_nsec / 1000)
                ),
                file_attributes: attributes.rawValue
            )
            
            var req = smb2_set_info_request()
            req.file_id = smb2_get_file_id(handle).pointee
            req.info_type = .init(SMB2_0_INFO_FILE)
            req.file_info_class = .init(SMB2_FILE_BASIC_INFORMATION)
            return withUnsafeMutablePointer(to: &bfi) { bfi in
                req.input_data = .init(bfi)
                return smb2_cmd_set_info_async(context, &req, SMB2Context.generic_handler, cbPtr)
            }
        }
    }

    func ftruncate(toLength: UInt64) throws {
        let handle = try handle.unwrap()
        try context.async_await { context, cbPtr -> Int32 in
            smb2_ftruncate_async(context, handle, toLength, SMB2Context.generic_handler, cbPtr)
        }
    }

    var maxReadSize: Int {
        (try? Int(context.withThreadSafeContext(smb2_get_max_read_size))) ?? -1
    }

    /// This value allows softer streaming
    var optimizedReadSize: Int {
        maxReadSize
    }

    @discardableResult
    func lseek(offset: Int64, whence: SeekWhence) throws -> Int64 {
        let handle = try handle.unwrap()
        let result = smb2_lseek(context.unsafeContext, handle, offset, whence.rawValue, nil)
        try POSIXError.throwIfError(result, description: context.error)
        return result
    }

    func read(length: Int = 0) throws -> Data {
        precondition(
            length <= UInt32.max, "Length bigger than UInt32.max can't be handled by libsmb2."
        )

        let handle = try handle.unwrap()
        let count = length > 0 ? length : optimizedReadSize
        var buffer = Data(repeating: 0, count: count)
        let result = try buffer.withUnsafeMutableBytes { buffer in
            try context.async_await { context, cbPtr -> Int32 in
                smb2_read_async(
                    context, handle, buffer.baseAddress, .init(buffer.count), SMB2Context.generic_handler, cbPtr
                )
            }
        }
        return Data(buffer.prefix(Int(result)))
    }

    func pread(offset: UInt64, length: Int = 0) throws -> Data {
        precondition(
            length <= UInt32.max, "Length bigger than UInt32.max can't be handled by libsmb2."
        )

        let handle = try handle.unwrap()
        let count = length > 0 ? length : optimizedReadSize
        var buffer = Data(repeating: 0, count: count)
        let result = try buffer.withUnsafeMutableBytes { buffer in
            try context.async_await { context, cbPtr -> Int32 in
                smb2_pread_async(
                    context, handle, buffer.baseAddress, .init(buffer.count), offset, SMB2Context.generic_handler,
                    cbPtr
                )
            }
        }
        return buffer.prefix(Int(result))
    }

    var maxWriteSize: Int {
        (try? Int(context.withThreadSafeContext(smb2_get_max_write_size))) ?? -1
    }

    var optimizedWriteSize: Int {
        maxWriteSize
    }

    func write<DataType: DataProtocol>(data: DataType) throws -> Int {
        precondition(
            data.count <= Int32.max, "Data bigger than Int32.max can't be handled by libsmb2."
        )

        let handle = try handle.unwrap()
        let result = try Data(data).withUnsafeBytes { buffer in
            try context.async_await { context, cbPtr -> Int32 in
                smb2_write_async(
                    context, handle, buffer.baseAddress, .init(buffer.count), SMB2Context.generic_handler, cbPtr
                )
            }
        }

        return Int(result)
    }

    func pwrite<DataType: DataProtocol>(data: DataType, offset: UInt64) throws -> Int {
        precondition(
            data.count <= Int32.max, "Data bigger than Int32.max can't be handled by libsmb2."
        )

        let handle = try handle.unwrap()
        let result = try Data(data).withUnsafeBytes { buffer in
            try context.async_await { context, cbPtr -> Int32 in
                smb2_pwrite_async(
                    context, handle, buffer.baseAddress, .init(buffer.count), offset, SMB2Context.generic_handler,
                    cbPtr
                )
            }
        }

        return Int(result)
    }

    func fsync() throws {
        let handle = try handle.unwrap()
        try context.async_await { context, cbPtr -> Int32 in
            smb2_fsync_async(context, handle, SMB2Context.generic_handler, cbPtr)
        }
    }
    
    func flock(_ op: LockOperation) throws {
        let handle = try handle.unwrap()
        try context.async_await_pdu { context, dataPtr in
            var element = smb2_lock_element(
                offset: 0,
                length: 0,
                flags: op.smb2Flag,
                reserved: 0
            )
            return withUnsafeMutablePointer(to: &element) { element in
                var request = smb2_lock_request(
                    lock_count: 1,
                    lock_sequence_number: 0,
                    lock_sequence_index: 0,
                    file_id: smb2_get_file_id(handle).pointee,
                    locks: element
                )
                return smb2_cmd_lock_async(context, &request, SMB2Context.generic_handler, dataPtr)
            }
        }
    }
    
    func changeNotify(for type: SMB2FileChangeType) throws {
        let handle = try handle.unwrap()
        try context.async_await_pdu { context, cbPtr in
            var request = smb2_change_notify_request(
                flags: UInt16(!type.intersection([.recursive]).isEmpty ? SMB2_CHANGE_NOTIFY_WATCH_TREE : 0),
                output_buffer_length: 0,
                file_id: smb2_get_file_id(handle).pointee,
                completion_filter: type.rawValue & 0x00ff_ffff
            )
            return smb2_cmd_change_notify_async(context, &request, SMB2Context.generic_handler, cbPtr)
        }
    }

    @discardableResult
    func fcntl<DataType: DataProtocol, R: IOCtlReply>(
        command: IOCtl.Command, args: DataType, needsReply _: Bool = true
    ) throws -> R {
        var inputBuffer = [UInt8](args)
        return try inputBuffer.withUnsafeMutableBytes { buf in
            var req = smb2_ioctl_request(
                ctl_code: command.rawValue,
                file_id: fileId.uuid,
                input_offset: 0, input_count: .init(buf.count),
                max_input_response: 0,
                output_offset: 0, output_count: UInt32(context.maximumTransactionSize),
                max_output_response: 65535,
                flags: .init(SMB2_0_IOCTL_IS_FSCTL),
                input: buf.baseAddress
            )
            return try context.async_await_pdu(dataHandler: R.init) {
                context, cbPtr -> UnsafeMutablePointer<smb2_pdu>? in
                smb2_cmd_ioctl_async(context, &req, SMB2Context.generic_handler, cbPtr)
            }.data
        }
    }

    func fcntl(command: IOCtl.Command) throws {
        let _: AnyIOCtlReply = try fcntl(command: command, args: Data(), needsReply: false)
    }

    func fcntl<DataType: DataProtocol>(command: IOCtl.Command, args: DataType) throws {
        let _: AnyIOCtlReply = try fcntl(command: command, args: args, needsReply: false)
    }

    func fcntl<R: IOCtlReply>(command: IOCtl.Command) throws -> R {
        try fcntl(command: command, args: [])
    }
}

extension SMB2FileHandle {
    struct SeekWhence: RawRepresentable, Sendable {
        var rawValue: Int32

        static let set = SeekWhence(rawValue: SEEK_SET)
        static let current = SeekWhence(rawValue: SEEK_CUR)
        static let end = SeekWhence(rawValue: SEEK_END)
    }
    
    struct LockOperation: OptionSet, Sendable {
        var rawValue: Int32
        
        static let shared = LockOperation(rawValue: LOCK_SH)
        static let exclusive = LockOperation(rawValue: LOCK_EX)
        static let unlock = LockOperation(rawValue: LOCK_UN)
        static let nonBlocking = LockOperation(rawValue: LOCK_NB)
        
        var smb2Flag: UInt32 {
            var result: UInt32 = 0
            if contains(.shared) { result |= 0x0000_0001 }
            if contains(.exclusive) { result |= 0x0000_0002 }
            if contains(.unlock) { result |= 0x0000_0004 }
            if contains(.nonBlocking) { result |= 0x0000_0010 }
            return result
        }
    }
    
    struct Attributes: OptionSet, Sendable {
        var rawValue: UInt32
        
        init(rawValue: UInt32) {
            self.rawValue = rawValue
        }
        
        static let readonly = Self(rawValue: SMB2_FILE_ATTRIBUTE_READONLY)
        static let hidden = Self(rawValue: SMB2_FILE_ATTRIBUTE_HIDDEN)
        static let system = Self(rawValue: SMB2_FILE_ATTRIBUTE_SYSTEM)
        static let directory = Self(rawValue: SMB2_FILE_ATTRIBUTE_DIRECTORY)
        static let archive = Self(rawValue: SMB2_FILE_ATTRIBUTE_ARCHIVE)
        static let normal = Self(rawValue: SMB2_FILE_ATTRIBUTE_NORMAL)
        static let temporary = Self(rawValue: SMB2_FILE_ATTRIBUTE_TEMPORARY)
        static let sparseFile = Self(rawValue: SMB2_FILE_ATTRIBUTE_SPARSE_FILE)
        static let reparsePoint = Self(rawValue: SMB2_FILE_ATTRIBUTE_REPARSE_POINT)
        static let compressed = Self(rawValue: SMB2_FILE_ATTRIBUTE_COMPRESSED)
        static let offline = Self(rawValue: SMB2_FILE_ATTRIBUTE_OFFLINE)
        static let notContentIndexed = Self(rawValue: SMB2_FILE_ATTRIBUTE_NOT_CONTENT_INDEXED)
        static let encrypted = Self(rawValue: SMB2_FILE_ATTRIBUTE_ENCRYPTED)
        static let integrityStream = Self(rawValue: SMB2_FILE_ATTRIBUTE_INTEGRITY_STREAM)
        static let noScrubData = Self(rawValue: SMB2_FILE_ATTRIBUTE_NO_SCRUB_DATA)
    }
    
    struct OpLock: OptionSet, Sendable {
        var rawValue: UInt8
        
        static let none = Self(rawValue: SMB2_OPLOCK_LEVEL_NONE)
        static let ii = Self(rawValue: SMB2_OPLOCK_LEVEL_II)
        static let exclusive = Self(rawValue: SMB2_OPLOCK_LEVEL_EXCLUSIVE)
        static let batch = Self(rawValue: SMB2_OPLOCK_LEVEL_BATCH)
        static let lease = Self(rawValue: SMB2_OPLOCK_LEVEL_LEASE)
    }
    
    enum ImpersonationLevel: UInt32, Hashable, Sendable {
        case anonymous = 0x00000000
        case identification = 0x00000001
        case impersonation = 0x00000002
        case delegate = 0x00000003
    }
    
    struct Access: OptionSet, Sendable {
        var rawValue: UInt32
        
        /* Access mask common to all objects */
        static let fileReadEA = Self(rawValue: SMB2_FILE_READ_EA)
        static let fileWriteEA = Self(rawValue: SMB2_FILE_WRITE_EA)
        static let fileDeleteChild = Self(rawValue: SMB2_FILE_DELETE_CHILD)
        static let fileReadAttributes = Self(rawValue: SMB2_FILE_READ_ATTRIBUTES)
        static let fileWriteAttributes = Self(rawValue: SMB2_FILE_WRITE_ATTRIBUTES)
        static let delete = Self(rawValue: SMB2_DELETE)
        static let readControl = Self(rawValue: SMB2_READ_CONTROL)
        static let writeDACL = Self(rawValue: SMB2_WRITE_DACL)
        static let writeOwner = Self(rawValue: SMB2_WRITE_OWNER)
        static let synchronize = Self(rawValue: SMB2_SYNCHRONIZE)
        static let acessSystemSecurity = Self(rawValue: SMB2_ACCESS_SYSTEM_SECURITY)
        static let maximumAllowed = Self(rawValue: SMB2_MAXIMUM_ALLOWED)
        static let genericAll = Self(rawValue: SMB2_GENERIC_ALL)
        static let genericExecute = Self(rawValue: SMB2_GENERIC_EXECUTE)
        static let genericWrite = Self(rawValue: SMB2_GENERIC_WRITE)
        static let genericRead = Self(rawValue: SMB2_GENERIC_READ)
        
        /* Access mask unique for file/pipe/printer */
        static let readData = Self(rawValue: SMB2_FILE_READ_DATA)
        static let writeData = Self(rawValue: SMB2_FILE_WRITE_DATA)
        static let appendData = Self(rawValue: SMB2_FILE_APPEND_DATA)
        static let execute = Self(rawValue: SMB2_FILE_EXECUTE)
        
        /* Access mask unique for directories */
        static let listDirectory = Self(rawValue: SMB2_FILE_LIST_DIRECTORY)
        static let addFile = Self(rawValue: SMB2_FILE_ADD_FILE)
        static let addSubdirectory = Self(rawValue: SMB2_FILE_ADD_SUBDIRECTORY)
        static let traverse = Self(rawValue: SMB2_FILE_TRAVERSE)
    }
    
    struct ShareAccess: OptionSet, Sendable {
        var rawValue: UInt32
        
        static let read = Self(rawValue: SMB2_FILE_SHARE_READ)
        static let write = Self(rawValue: SMB2_FILE_SHARE_WRITE)
        static let delete = Self(rawValue: SMB2_FILE_SHARE_DELETE)
    }
    
    enum CreateDisposition: UInt32, Sendable {        
        case supersede = 0x00000000 // SMB2_FILE_SUPERSEDE
        case open = 0x00000001 // SMB2_FILE_OPEN
        case create = 0x00000002 // SMB2_FILE_CREATE
        case openIfExists = 0x00000003 // SMB2_FILE_OPEN_IF
        case overwrite = 0x00000004 // SMB2_FILE_OVERWRITE
        case overwriteIfExists = 0x00000005 // SMB2_FILE_OVERWRITE_IF
    }
    
    struct CreateOptions: OptionSet, Sendable {
        var rawValue: UInt32
        
        static let directoryFile = Self(rawValue: SMB2_FILE_DIRECTORY_FILE)
        static let writeThrough = Self(rawValue: SMB2_FILE_WRITE_THROUGH)
        static let sequentialOnly = Self(rawValue: SMB2_FILE_SEQUENTIAL_ONLY)
        static let noIntermediateBuffering = Self(rawValue: SMB2_FILE_NO_INTERMEDIATE_BUFFERING)
        static let synchronousIOAlert = Self(rawValue: SMB2_FILE_SYNCHRONOUS_IO_ALERT)
        static let synchronousIONonAlert = Self(rawValue: SMB2_FILE_SYNCHRONOUS_IO_NONALERT)
        static let nonDirectoryFile = Self(rawValue: SMB2_FILE_NON_DIRECTORY_FILE)
        static let completeIfOplocked = Self(rawValue: SMB2_FILE_COMPLETE_IF_OPLOCKED)
        static let noEAKnowledge = Self(rawValue: SMB2_FILE_NO_EA_KNOWLEDGE)
        static let randomAccess = Self(rawValue: SMB2_FILE_RANDOM_ACCESS)
        static let deleteOnClose = Self(rawValue: SMB2_FILE_DELETE_ON_CLOSE)
        static let openByFileID = Self(rawValue: SMB2_FILE_OPEN_BY_FILE_ID)
        static let openForBackupIntent = Self(rawValue: SMB2_FILE_OPEN_FOR_BACKUP_INTENT)
        static let noCompression = Self(rawValue: SMB2_FILE_NO_COMPRESSION)
        static let openRemoteInstance = Self(rawValue: SMB2_FILE_OPEN_REMOTE_INSTANCE)
        static let openRequiringOplock = Self(rawValue: SMB2_FILE_OPEN_REQUIRING_OPLOCK)
        static let disallowExclusive = Self(rawValue: SMB2_FILE_DISALLOW_EXCLUSIVE)
        static let reserveOpfilter = Self(rawValue: SMB2_FILE_RESERVE_OPFILTER)
        static let openReparsePoint = Self(rawValue: SMB2_FILE_OPEN_REPARSE_POINT)
        static let openNoRecall = Self(rawValue: SMB2_FILE_OPEN_NO_RECALL)
        static let openForFreeSpaceQuery = Self(rawValue: SMB2_FILE_OPEN_FOR_FREE_SPACE_QUERY)
    }
}

extension RawRepresentable where RawValue == UInt32 {
    init(rawValue: Int32) {
        self.init(rawValue: .init(bitPattern: rawValue))!
    }
}

extension RawRepresentable where RawValue: BinaryInteger {
    init(rawValue: Int32) {
        self.init(rawValue: .init(truncatingIfNeeded: rawValue))!
    }
}
